/**
 * Copyright (c) 2018-2023, NXOS Development Team
 * SPDX-License-Identifier: Apache-2.0
 *
 * Contains: Virtio BUS - PCI
 *
 * Change Logs:
 * Date           Author            Notes
 * 2023-07-03     Shiroko           Init
 */
#include <nxos.h>

#ifdef CONFIG_NX_DRIVER_VIRTIO_PCI

#undef NX_LOG_MOD_NAME
#define NX_LOG_MOD_NAME "VIRTIO-PCI"

#include <base/log.h>
#include <pci.h>

#include "virtio.h"
#include "virtio_pci.h"
#include "virtio_pci_private.h"

// Common config
void VirtioPCICommonConfigGet(NX_Device *device, VirtioCommonConfigItem item, void *value)
{
    VirtioPCICommonCfg *commonCfg = VirtioGetBusExtension(device, VirtioPCIExtension)->commonCfg;
    switch (item)
    {
    case VIRTIO_COMMON_CFG_FEATURE_DEVICE:
        commonCfg->device_feature_select = ((VirtioFeature *) value)->selector;
        NX_MemoryBarrier();
        ((VirtioFeature *) value)->features = commonCfg->device_feature;
        break;
    case VIRTIO_COMMON_CFG_FEATURE_DRIVER:
        commonCfg->driver_feature_select = ((VirtioFeature *) value)->selector;
        NX_MemoryBarrier();
        ((VirtioFeature *) value)->selector = commonCfg->driver_feature;
        break;
    case VIRTIO_COMMON_CFG_DEVICE_STATUS:
        *((NX_U8 *) value) = commonCfg->device_status;
        break;
    case VIRTIO_COMMON_CFG_QUEUE_NUM_MAX:
        *((NX_U16 *) value) = commonCfg->num_queues;
        break;
    case VIRTIO_COMMON_CFG_QUEUE_SELECT:
        *((NX_U16 *) value) = commonCfg->queue_select;
        break;
    case VIRTIO_COMMON_CFG_QUEUE_DESC:
        *((NX_U64 *) value) = commonCfg->queue_desc;
        break;
    case VIRTIO_COMMON_CFG_QUEUE_DRIVER:
        *((NX_U64 *) value) = commonCfg->queue_driver;
        break;
    case VIRTIO_COMMON_CFG_QUEUE_DEVICE:
        *((NX_U64 *) value) = commonCfg->queue_device;
        break;
    case VIRTIO_COMMON_CFG_QUEUE_ENABLE:
        *((NX_U16 *) value) = commonCfg->queue_enable;
        break;
    case VIRTIO_COMMON_CFG_QUEUE_SIZE:
        *((NX_U16 *) value) = commonCfg->queue_size;
        break;
    case VIRTIO_COMMON_CFG_CONFIG_GENERATION:
        *((NX_U8 *) value) = commonCfg->config_generation;
        break;
    }
}

void VirtioPCICommonConfigSet(NX_Device *device, VirtioCommonConfigItem item, void *value)
{
    VirtioPCICommonCfg *commonCfg = VirtioGetBusExtension(device, VirtioPCIExtension)->commonCfg;
    switch (item)
    {
    case VIRTIO_COMMON_CFG_FEATURE_DEVICE:
        NX_PANIC("Feature_Device of Common Config is Read-only.");
        break;
    case VIRTIO_COMMON_CFG_FEATURE_DRIVER:
        commonCfg->driver_feature_select = ((VirtioFeature *) value)->selector;
        NX_MemoryBarrier();
        commonCfg->driver_feature = ((VirtioFeature *) value)->features;
        break;
    case VIRTIO_COMMON_CFG_DEVICE_STATUS:
        commonCfg->device_status = *((NX_U8 *) value);
        break;
    case VIRTIO_COMMON_CFG_QUEUE_NUM_MAX:
        commonCfg->num_queues = *((NX_U16 *) value);
        break;
    case VIRTIO_COMMON_CFG_QUEUE_SELECT:
        commonCfg->queue_select = *((NX_U16 *) value);
        break;
    case VIRTIO_COMMON_CFG_QUEUE_DESC:
        commonCfg->queue_desc = *((NX_U64 *) value);
        break;
    case VIRTIO_COMMON_CFG_QUEUE_DRIVER:
        commonCfg->queue_driver = *((NX_U64 *) value);
        break;
    case VIRTIO_COMMON_CFG_QUEUE_DEVICE:
        commonCfg->queue_device = *((NX_U64 *) value);
        break;
    case VIRTIO_COMMON_CFG_QUEUE_ENABLE:
    {
        NX_U16 enabled = *((NX_U16 *) value);
        if (enabled == VIRTIO_COMMON_CFG_QUEUE_ENABLE_DISABLE)
            break; // setting queue_enable to 0 is invalid in PCI Bus
        commonCfg->queue_enable = enabled;
        break;
    }
    case VIRTIO_COMMON_CFG_QUEUE_SIZE:
    {
        NX_U16 queue_size = *((NX_U16 *) value);
        commonCfg->queue_size = queue_size;
        break;
    }
    case VIRTIO_COMMON_CFG_CONFIG_GENERATION:
        NX_PANIC("Virtio Config Generation is Read-Only.");
        break;
    }
}

// Device config
void *VirtioPCIGetDeviceConfig(NX_Device *device)
{
    return VirtioGetBusExtension(device, VirtioPCIExtension)->deviceCfg;
}

NX_PRIVATE void *VirtioPCIRemapBar(NX_List *mappedBarListHead, NX_PciDeviceBar *bar)
{
    NX_Bool alreadyMapped = NX_False;
    VirtioPCIMappedBar *mappedBar;
    NX_ListForEachEntry(mappedBar, mappedBarListHead, list)
    {
        if (mappedBar->bar == bar)
        {
            alreadyMapped = NX_True;
            break;
        }
    }
    if (!alreadyMapped)
    {
        NX_Addr virAddr = VirtioVirtualAddressAllocate(bar->base_addr);
        NX_MmuMapPageWithPhy(&gKernelMmu, virAddr, bar->base_addr, bar->length,
                             NX_PAGE_ATTR_READ | NX_PAGE_ATTR_WRITE | NX_PAGE_ATTR_KERNEL);
        NX_MmuFlush();
        mappedBar = (VirtioPCIMappedBar *) NX_MemAlloc(sizeof(VirtioPCIMappedBar));
        mappedBar->bar = bar;
        NX_ASSERT(mappedBar != NX_NULL);
        NX_ListInit(&mappedBar->list);
        NX_ListAddTail(&mappedBar->list, mappedBarListHead);
    }
    return (void *) bar->base_addr;
}

NX_PRIVATE void VirtioPCIUnmapBar(NX_List *mappedBarListHead)
{
    VirtioPCIMappedBar *head, *next;

    NX_ListForEachEntrySafe(head, next, mappedBarListHead, list)
    {
        NX_Addr virAddr = VirtioVirtualAddressAllocate(head->bar->base_addr);
        NX_MmuUnmapPage(&gKernelMmu, virAddr, head->bar->length, NX_True);
        NX_ListDelInit(&head->list);
        NX_MemFree(head);
    }
    NX_MmuFlush();
}


NX_Device *VirtioPCIInitDevice(NX_Driver *driver, const char *dev_name, NX_PciDevice *pci_device)
{
    NX_LOG_D("Initialize PCI Device %x:%x bus %d dev %d for %s",
             pci_device->vendor_id, pci_device->device_id, pci_device->bus, pci_device->dev, dev_name);
    NX_Device *device = NX_NULL;
    if (NX_DriverAttachDevice(driver, dev_name, &device) != NX_EOK)
    {
        NX_LOG_E("NX_DriverAttachDevice %s failed!", dev_name);
        return NX_NULL;
    }

    // Setup device extension field
    VirtioExtension *deviceExtension = NX_MemAlloc(sizeof(VirtioExtension));
    if (deviceExtension == NX_NULL) goto failed;
    device->extension = (void *) deviceExtension;
    deviceExtension->device = device;
    deviceExtension->busType = VIRTIO_BUS_TYPE_PCI;

    VirtioPCIExtension *pciExtension = NX_MemAlloc(sizeof(VirtioPCIExtension));
    if (pciExtension == NX_NULL) goto failed;

    deviceExtension->busExt = (void *) pciExtension;
    pciExtension->pciDevice = pci_device;

    NX_ListInit(&deviceExtension->list);
    NX_ListInit(&pciExtension->mappedBarListHead);

    // Enumerate PCI Capacities
    NX_ASSERT((pci_device->status & PCI_STATUS_CAPABILITIES_LIST) != 0);
    NX_U32 capacityPointer = pci_device->capability_list;
    for (; capacityPointer;)
    {
        NX_U32 capacityBuf[sizeof(VirtioPCICapacity) / sizeof(NX_U32)];
        VirtioPCICapacity *capacity = (VirtioPCICapacity *) capacityBuf;
        int offset;
        for (offset = 0; offset < sizeof(VirtioPCICapacity); offset += 4)
            capacityBuf[offset / 4] = NX_PciDeviceRead(pci_device, capacityPointer + offset);
        if (capacity->vendor == VIRTIO_PCI_CAPACITY_VENDOR)
        {
            NX_PciDeviceBar *bar = &pci_device->bars[capacity->bar];
            switch (capacity->type)
            {
            case VIRTIO_PCI_CAPACITY_COMMON_CFG:
            {
                pciExtension->commonCfg = (VirtioPCICommonCfg *) (
                        VirtioPCIRemapBar(&pciExtension->mappedBarListHead, bar) + capacity->offset);
                break;
            }
            case VIRTIO_PCI_CAPACITY_DEVICE_CFG:
            {
                pciExtension->deviceCfg = (VirtioPCICommonCfg *) (
                        VirtioPCIRemapBar(&pciExtension->mappedBarListHead, bar) + capacity->offset);
                break;
            }
            case VIRTIO_PCI_CAPACITY_ISR_CFG:
            {
                pciExtension->isrStatus = (NX_U32 *) (
                        VirtioPCIRemapBar(&pciExtension->mappedBarListHead, bar) + capacity->offset);
                break;
            }
            case VIRTIO_PCI_CAPACITY_NOTIFY_CFG:
            {
                void *notifyBaseAddr =
                        VirtioPCIRemapBar(&pciExtension->mappedBarListHead, bar) + capacity->offset;
                NX_U32 notifyOffMultiplier =
                        NX_PciDeviceRead(pci_device, capacityPointer + sizeof(VirtioPCICapacity));
                pciExtension->queueNotifyBase = notifyBaseAddr;
                pciExtension->queueNotifyMultiplier = notifyOffMultiplier;
                break;
            }
            default:
                break;
            }
        }
        capacityPointer = capacity->next;
    }
    if (pciExtension->commonCfg == NX_NULL || pciExtension->isrStatus == NX_NULL ||
        pciExtension->queueNotifyBase == NX_NULL)
    {
        NX_LOG_E("PCI Device %x:%x bus %d dev %d missed some needed capabilities.",
                 pci_device->vendor_id, pci_device->device_id, pci_device->bus, pci_device->dev
        );
        goto failed;
    }
    // Enabling Bus Mastering
    NX_PciEnableBusMastering(pci_device);
    return device;
failed:
    VirtioPCICleanUpDevice(device);
    return NX_NULL;
}

NX_PRIVATE NX_Error VirtioPCISetupISR(NX_Device *device);
NX_PRIVATE void VirtioPCIRemoveISR(NX_Device *device);

void VirtioPCICleanUpDevice(NX_Device *device)
{
    char deviceName[NX_DEVICE_NAME_LEN];
    NX_MemCopy(deviceName, device->name, NX_DEVICE_NAME_LEN);

    NX_Driver *driver = device->driver;
    if (driver == NX_NULL) return;

    VirtioExtension *deviceExtension = VirtioGetExtension(device);
    if (deviceExtension == NX_NULL) goto final;

    VirtioPCIExtension *pciExtension = VirtioGetBusExtension(device, VirtioPCIExtension);
    if (pciExtension == NX_NULL)
    {
        NX_MemFree(deviceExtension);
        goto final;
    }

    VirtioPCIUnmapBar(&pciExtension->mappedBarListHead);
    VirtioPCIRemoveISR(device);

    NX_ASSERT(deviceExtension->queue == NX_NULL); // Must free queue before do clean up

    NX_MemFree(pciExtension);
    NX_MemFree(deviceExtension);
final:
    NX_DriverDetachDevice(driver, device->name);
    NX_LOG_I("Device %s detached.", deviceName);
}


NX_Error VirtioPCISetup(VirtioDriverTableEntity **driversTable)
{
    // Discover devices over PCI Bus
    NX_PciDevice *pci_device;
    VirtioDeviceId deviceId;

    NX_ListForEachEntry(pci_device, NX_PciDeviceListGet(), list)
    {
        if (pci_device->vendor_id == VIRTIO_PCI_VENDOR_ID)
        {
            if (VIRTIO_PCI_DEVICE_ID_IS_TRANSITIONAL(pci_device->device_id))
            {
                // Legacy devices are not supported yet (not too hard but let us focus on modern devices now)
                continue;
                // VirtIO Device is legacy
                // Transitional devices MUST have a PCI Revision ID of 0.
                NX_ASSERT(pci_device->revision_id != 0);
                // Transitional devices MUST have the PCI Subsystem Device ID matching the Virtio Device ID
                deviceId = (VirtioDeviceId)pci_device->subsystem_device_id;
            }
            else
            {
                // VirtIO Device is modern
                deviceId = (VirtioDeviceId)GET_VIRTIO_DEVICE_ID_FROM_PCI(pci_device->device_id);
                NX_ASSERT(deviceId > VIRTIO_DEVICE_ID_INVALID && deviceId < VIRTIO_DEVICE_TYPE_SIZE);
                VirtioDriverTableEntity *entity = driversTable[deviceId];
                if (entity)
                {
                    // We have the corresponding driver registered.
                    char deviceName[NX_DEVICE_NAME_LEN];
                    NX_SNPrintf(deviceName, NX_DEVICE_NAME_LEN, entity->deviceNameTemplate, entity->deviceCount);
                    NX_LOG_D("Creating device %s...", deviceName);
                    NX_Device *device = VirtioPCIInitDevice(entity->driver, deviceName, pci_device);
                    if (device)
                    {
                        // Create device successful
                        entity->deviceCount++;
                        NX_ListAddTail(&entity->deviceListHead,
                                       &(VirtioGetExtension(device)->list));
                        if (VirtioPCISetupISR(device) != NX_EOK)
                            NX_LOG_W("ISR Setup for device %s failed. Features may not be functional.", deviceName);
                        if (entity->initHandler)
                            if (entity->initHandler(entity->driver, device) != NX_EOK)
                            {
                                NX_LOG_E("Initialize %s failed.", deviceName);
                                VirtioPCICleanUpDevice(device);
                                return NX_EFAULT;
                            }
                        NX_LOG_I("Create %s successfully.", deviceName);
                    }
                }
                else
                {
                    NX_LOG_W("Driver for DeviceID %d is missing!", deviceId);
                }
            }
        }
    }
    return NX_EOK;
}

void VirtioPCISendNotification(NX_Device *device, NX_U16 queue_index)
{
    VirtioPCIExtension *pciExtension = VirtioGetBusExtension(device, VirtioPCIExtension);
    if (pciExtension->queueNotifyMultiplier == 0)
        *((NX_U16 *) pciExtension->queueNotifyBase) = queue_index;
    else
    {
        pciExtension->commonCfg->queue_select = queue_index;
        *((NX_U16 *) (pciExtension->queueNotifyBase +
                      pciExtension->commonCfg->queue_notify_off * pciExtension->queueNotifyMultiplier))
                = queue_index;
    }
}

/* Interrupt Service Routine */

NX_PRIVATE NX_Error VirtioPCI_ISR(NX_IRQ_Number irqNumber, void *data)
{
    NX_Device *device = (NX_Device *) data;
    VirtioPCIExtension *pciExtension = VirtioGetBusExtension(device, VirtioPCIExtension);
    // Read ISR Status Field
    // Reading to ISR Register make it an ack notify and reset it to zero
    NX_U32 isrStatus = *pciExtension->isrStatus;
    if (isrStatus & 1)
    {
        // Bit 0 set, queue interrupt
        return VirtioISR(VIRTIO_ISR_TYPE_QUEUE, device);
    }
    else if (isrStatus & 2)
    {
        // Bit 1 set, configuration interrupt
        return VirtioISR(VIRTIO_ISR_TYPE_CONFIG, device);
    }
    return NX_EINVAL;
}

NX_PRIVATE NX_Error VirtioPCISetupISR(NX_Device *device)
{
    VirtioSetupISR(device);
    NX_PciDevice *pciDevice = VirtioGetBusExtension(device, VirtioPCIExtension)->pciDevice;
    // NXOS is using old PIC for x86 platform.
    if (NX_IRQ_Bind(pciDevice->irq_line, VirtioPCI_ISR, (void *) device, device->name, 0) != NX_EOK)
        return NX_EFAULT;
    if (NX_IRQ_Unmask(pciDevice->irq_line) != NX_EOK)
        return NX_EFAULT;
    return NX_EOK;
}


NX_PRIVATE void VirtioPCIRemoveISR(NX_Device *device)
{
    NX_PciDevice *pciDevice = VirtioGetBusExtension(device, VirtioPCIExtension)->pciDevice;
    // NXOS is using old PIC for x86 platform. This action may not always successful.
    NX_IRQ_Unbind(pciDevice->irq_line, (void *) device);
    VirtioRemoveISR(device);
}

#endif // CONFIG_NX_DRIVER_VIRTIO_PCI
